/*
 * Copyright 2010 Lukas Benda <luk.benda AT gmail.com>
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *    http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cz.apnetis.gwt.client;

import java.util.Map;

import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.HasChangeHandlers;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.event.dom.client.HasKeyPressHandlers;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.FocusWidget;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
import com.google.gwt.user.client.ui.HasText;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.HasWordWrap;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.TextArea;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.TextBoxBase;
import com.google.gwt.user.client.ui.Widget;

/** Editable Label class, functionality displays a Label UI
 * Element until clicked on, then if element is set to be
 * editable (default) then an editable area and Buttons are
 * displayed instead.
 *
 * If the Label is not set to be word wrapped (default) then
 * the editable area is a Text Box and clicking the OK button
 * or hitting return key in the TextBox will display the Label with
 * the updated text.
 *
 * If the Label is set to be word wrapped, using the setWordWrap(boolean)
 * method, then the editable area is a Text Area and clicking the OK
 * button will display the Label with the updated text.
 *
 * In both cases, clicking Cancel button or hitting Escape key in the
 * TextBox/TextArea then the Label is displayed with original text.
 * 
 * @param <E> type of value which edit label return
 * @author Lukas Benda
 * @version 1.1
 */
public class EditableLabel<E> extends Composite implements HasWordWrap, HasText, HasHorizontalAlignment, HasClickHandlers, HasValueChangeHandlers<E>, HasValue<E> {

  /** Container for change edit widget. */
  private final Panel editWidgetPanel = new SimplePanel();
  /** TextBox element to enable text to be changed if Label is not word wrapped */
  private TextBoxBase changeText;
  /** List box if editable label is represent by list box */
  private ListBox listBox;
  /** Label element, which is initially is displayed. */
  private Label text;
  /** String element that contains the original text of a Label prior to it being
   * edited. */
  private String originalText;

  /** Simple button to confirm changes */
  private Widget confirmChange;
  /** Simple button to cancel changes */
  private Widget cancelChange;

  /** Flag to indicate that Label is in editing mode. */
  private boolean isEditing = false;
  /** Flag to indicate that label can be edited. */
  private boolean isEditable = true;
  /** Flag which inform if edit box have ok and cancel button */
  private boolean hasButtons;

  /** Default String value for OK button */
  private final String defaultOkButtonText = "OK";
  /** Default String value for Cancel button */
  private final String defaultCancelButtonText = "Cancel";

  /** Converter for convert value to text and back. If is null, then value is transfrom by String.ValueOf()*/
  private StringConverter<E> stringConverter; public void setStringConverter(final StringConverter<E> stringConverter) { this.stringConverter = stringConverter; } public StringConverter<E> getStringConverter() { return this.stringConverter; }

  /** Allows the setting of the isEditable flag, marking the label as editable or not.
   * @param flag True or False value depending if the Label is to be editable or not
   */
  private EditableType editableType; public void setEditable(final boolean flag) {this.isEditable = flag; }

  /** Returns the value of the isEditable flag. */
  public boolean isFieldEditable() {
    return this.isEditable;
  }

  /** Returns the value of the isEditing flag, allowing outside users to see if
   * the Label is being edited or not.
   */
  public boolean isInEditingMode() {
    return this.isEditing;
  }

  /** Change the displayed label to be a TextBox and copy label text into the
   * TextBox.
   */
  private void changeTextLabel() {
    if (this.isEditable) {
      // Set up the TextBox
      this.originalText = this.text.getText();

      // Change the view from Label to TextBox and Buttons
      this.text.setVisible(false);
      if (isHasButtons()) {
        this.confirmChange.setVisible(true);
        this.cancelChange.setVisible(true);
      }

      this.editWidgetPanel.setVisible(true);
      setEditableText(this.originalText);
      ((FocusWidget) this.editWidgetPanel.iterator().next()).setFocus(true);

      // Set instance as being in editing mode.
      this.isEditing = true;
    }
  }

  /** Restores visibility of Label and hides the TextBox and Buttons */
  private void restoreVisibility() {
    // Change appropriate visibilities
    this.text.setVisible(true);
    if (isHasButtons()) {
      this.confirmChange.setVisible(false);
      this.cancelChange.setVisible(false);
    }
    this.editWidgetPanel.setVisible(false);

    // Set isEditing flag to false as we are no longer editing
    this.isEditing = false;
  }

  /** Sets the Label text to the new value, restores the display and calls the
   * update method. */
  private void setTextLabel() {
    // Set the Label to be the text in the Text Box
    this.text.setText(getEditableText());

    // Set the object back to display label rather than TextBox and Buttons
    restoreVisibility();

    // Call the update method provided in the Constructor
    // (this could be anything from alerting the user through to
    // Making an AJAX call to store the data.
    // updater.onChange(this);
    ValueChangeEvent.fireIfNotEqual(this, getStringConverter().stringToValue(this.originalText),
        getStringConverter().stringToValue(getEditableText()));
  }

  /** Sets the Label text to the original value, restores the display. */
  public void cancelLabelChange() {
    // Set the Label text back to what it was originally
    this.text.setText(this.originalText);
    // Set the object back to display Label rather than TextBox and Buttons
    restoreVisibility();
  }

  /** Creates the Label, the TextBox and Buttons. Also associates the update
   * method provided in the constructor with this instance.
   * @param labelText The value of the initial Label.
   * @param visibleLength The visible length (width) of the TextBox/TextArea.
   * @param maxLength The maximum length of text in the TextBox.
   * @param maxHeight The maximum number of visible lines of the TextArea
   * @param okButtonText The text diplayed in the OK button.
   * @param cancelButtonText The text displayed in the Cancel button.
   */
  private void createEditableLabel(final String labelText, final String okButtonText,
      final String cancelButtonText) {
    // Put everything in a VerticalPanel
    final FlowPanel instance = new FlowPanel();

    // Create the Label element and add a ClickHandler to call out Change
    // method when clicked
    this.text = new Label(labelText);
    this.text.setStyleName("editableLabel-label");

    this.text.addClickHandler(new ClickHandler() {
      @Override
      public void onClick(final ClickEvent event) {
        changeTextLabel();
      }
    });

    // Set up Confirmation Button
    this.confirmChange = createConfirmButton(okButtonText);

    if (!(this.confirmChange instanceof HasClickHandlers)) {
      throw new RuntimeException(
      "Confirm change button must allow for click events");
    }

    ((HasClickHandlers) this.confirmChange).addClickHandler(new ClickHandler() {
      @Override
      public void onClick(final ClickEvent event) {
        setTextLabel();
      }
    });

    // Set up Cancel Button
    this.cancelChange = createCancelButton(cancelButtonText);
    if (!(this.cancelChange instanceof HasClickHandlers)) {
      throw new RuntimeException(
      "Cancel change button must allow for click events");
    }

    ((HasClickHandlers) this.cancelChange).addClickHandler(new ClickHandler() {
      @Override
      public void onClick(final ClickEvent event) {
        cancelLabelChange();
      }
    });

    // Put the buttons in a panel
    final FlowPanel buttonPanel = new FlowPanel();
    buttonPanel.setStyleName("editableLabel-buttonPanel");
    buttonPanel.add(this.confirmChange);
    buttonPanel.add(this.cancelChange);

    // Add panels/widgets to the widget panel
    instance.add(this.text);
    instance.add(this.editWidgetPanel);
    instance.add(buttonPanel);

    // Set initial visibilities. This needs to be after
    // adding the widgets to the panel because the FlowPanel
    // will mess them up when added.
    this.text.setVisible(true);
    this.editWidgetPanel.setVisible(false);

    this.confirmChange.setVisible(false);
    this.cancelChange.setVisible(false);

    // Assume that this is a non word wrapped Label unless explicitly set
    // otherwise
    this.text.setWordWrap(false);

    // Set the widget that this Composite represents
    initWidget(instance);
  }

  /** Return editable widget */
  private final Widget getEditableWidget() {
    switch (getEditableType()) {
      case LISTBOX :
        if (this.listBox == null) {
          this.listBox = new ListBox();
          addChangeHandlers(this.listBox);
          addKeyPressedHandlers(this.listBox);
          this.editWidgetPanel.clear();
          this.editWidgetPanel.add(this.listBox);
        }
        return this.listBox;
      default :
        if (this.changeText == null) {
          if (this.getWordWrap()) {
            this.changeText = new TextArea();
            this.changeText.setStyleName("editableLabel-textArea");
          } else {
            this.changeText = new TextBox();
            this.changeText.setStyleName("editableLabel-textBox");
          }
          addChangeHandlers(this.changeText);
          addKeyPressedHandlers(this.changeText);
          this.editWidgetPanel.clear();
          this.editWidgetPanel.add(this.changeText);
        }
        return this.changeText;
    }
  }

  /** Method return text from editable item */
  private final String getEditableText() {
    switch (getEditableType()) {
      case LISTBOX : return this.listBox.getItemText(this.listBox.getSelectedIndex());
      default : return this.changeText.getText();
    }
  }

  /** Set text of editable item */
  private final void setEditableText(final String text) {
    switch (getEditableType()) {
      case LISTBOX :
        for (int i = 0; i < this.listBox.getItemCount(); i++) {
          if (this.listBox.getItemText(i).equals(text)) {
            this.listBox.setSelectedIndex(i);
          }
        }
        break;
      default : ((TextBoxBase) getEditableWidget()).setText(text);
    }
  }

  private final void addChangeHandlers(final HasChangeHandlers widget) {
    widget.addChangeHandler(new ChangeHandler() {
      @Override
      public void onChange(final ChangeEvent event) {
        if (getEditableText() != null) {
          if (!getEditableText().equals(EditableLabel.this.originalText)) {
            setTextLabel();
          }
        }
      }
    });
  }

  /** Add change handlers to key pressed handlers to editable item */
  private final void addKeyPressedHandlers(final HasKeyPressHandlers editableWidget) {
    editableWidget.addKeyPressHandler(new KeyPressHandler() {
      @Override
      public void onKeyPress(final KeyPressEvent event) {
        if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
          setTextLabel();
        } else if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ESCAPE) {
          cancelLabelChange();
        }
      }
    });
  }

  /**
   * @param cancelButtonText
   */
  protected Widget createCancelButton(final String cancelButtonText) {
    final Button result = new Button();
    result.setStyleName("editableLabel-buttons");
    result.addStyleName("editableLabel-cancel");
    result.setText(cancelButtonText);
    return result;
  }

  /**
   * @param okButtonText
   */
  protected Widget createConfirmButton(final String okButtonText) {
    final Button result = new Button();
    result.setStyleName("editableLabel-buttons");
    result.addStyleName("editableLabel-confirm");
    result.setText(okButtonText);
    return result;
  }

  /** Set the word wrapping on the label (if word wrapped then the editable field
   * becomes a TextArea, if not then the editable field is a TextBox.
   * @param b Boolean value, true means Label is word wrapped, false means it is not.
   */
  @Override
  public final void setWordWrap(final boolean b) {
    this.text.setWordWrap(b);
  }

  /** Return whether the Label is word wrapped or not. */
  @Override
  public final boolean getWordWrap() {
    return this.text.getWordWrap();
  }

  /** Return the text value of the Label */
  @Override
  public final String getText() {
    return this.text.getText();
  }

  /** Set the text value of the Label */
  @Override
  public final void setText(final String newText) {
    this.text.setText(newText);
  }

  /** Method return value of edit box. If it's text box or text area then return
   * result of getText(), if is list box then return value of selected item.
   * @return text or or value of selected item
   */
  @Override
  public final E getValue() {
    switch (getEditableType()) {
      case LISTBOX : return getStringConverter().stringToValue(this.listBox.getValue(this.listBox.getSelectedIndex()));
      default : return getStringConverter().stringToValue(getText());
    }
  }

  /** Set value of edit label. */
  @Override
  @SuppressWarnings("unchecked")
  public final void setValue(final E value) {
    if (getStringConverter() == null) {
      setStringConverter((StringConverter<E>) StringConverter.DEFAULT_CONVERTERS.get(value.getClass()));
    }
    switch (getEditableType()) {
      case LISTBOX :
        for (int i = 0; i < ((ListBox) getEditableWidget()).getItemCount(); i++) {
          if (value.equals(this.listBox.getValue(i))) {
            ((ListBox) getEditableWidget()).setSelectedIndex(i);
            this.text.setText(((ListBox) getEditableWidget()).getItemText(i));
          }
        }
        break;
      default : setText(getStringConverter().valueToString(value));
    }
  }

  @Override
  public void setValue(final E value, final boolean fireEvents) {
    setValue(value);
    ValueChangeEvent.fire(this, value);
  }

  /** Add item of list box. If type isn't list box calling of this method change
   * type to list box
   * @param newText text of list box item
   * @param newValue value of list box item
   */
  public final void addListItem(final String newText, final String newValue) {
    setEditableType(EditableType.LISTBOX);
    ((ListBox) getEditableWidget()).addItem(newText, newValue);
  }

  /** Add item of list box. If type isn't list box calling of this method change
   * type to list box
   * @param items value -> text of list box
   */
  public final void addListItems(final Map<String, String> items) {
    setEditableType(EditableType.LISTBOX);
    for (final Map.Entry<String, String> entry : items.entrySet()) {
      ((ListBox) getEditableWidget()).addItem(entry.getValue(), entry.getKey());
    }
  }

  /** Sets the number of visible lines for a word-wrapped editable label.
   * @param number Number of visible lines.
   * @throws RuntimeException if the editable label is not word-wrapped.
   */
  public final void setVisibleLines(final int number) {
    if (this.editableType != EditableType.TEXTAREA) {
      throw new RuntimeException("Cannnot set number of visible lines for a non "
          + "word-wrapped Editable Label. Or for lablel which is represent by list box.");
    }
    if (this.text.getWordWrap()) {
      ((TextArea) this.changeText).setVisibleLines(number);
    } else {
      throw new RuntimeException("Cannnot set number of visible lines for a non word-wrapped Editable Label");
    }
  }

  /** Get the number of Visible Lines of editable area of a word-wrapped editable
   * Label.
   * @return Number of Visible Lines.
   * @throws RuntimeException If the Label is not word-wrapped.
   */
  public final int getVisibleLines() {
    if (this.editableType != EditableType.TEXTAREA) {
      throw new RuntimeException("Cannnot get number of visible lines for a non "
          + "word-wrapped Editable Label. Or for lablel which is represent by list box.");
    }
    if (this.text.getWordWrap()) {
      return ((TextArea) this.changeText).getVisibleLines();
    } else {
      throw new RuntimeException("Editable Label that is not word-wrapped has no number of Visible Lines");
    }
  }

  /** Set maximum length of editable area.
   * @param length Length of editable area.
   */
  public final void setMaxLength(final int length) {
    if (this.editableType == EditableType.LISTBOX) { throw new RuntimeException("Cann't set max length for list box."); }
    if (this.text.getWordWrap()) {
      ((TextArea) this.changeText).setCharacterWidth(length);
    } else {
      ((TextBox) this.changeText).setMaxLength(length);
    }
  }

  /** Get maximum length of editable area.
   * @return maximum length of editable area.
   */
  public final int getMaxLength() {
    if (this.editableType == EditableType.LISTBOX) { throw new RuntimeException("Cann't get max length for list box."); }
    if (this.text.getWordWrap()) {
      return ((TextArea) this.changeText).getCharacterWidth();
    } else {
      return ((TextBox) this.changeText).getMaxLength();
    }
  }

  /** Set the visible length of the editable area.
   * @throws RuntimeExcpetion If editable label is word wrapped.
   */
  public final void setVisibleLength(final int length) {
    if (this.editableType != EditableType.TEXTBOX) { throw new RuntimeException("Cannnot set visible length for a word-wrapped Editable Label."); }
    if (this.text.getWordWrap()) {
      throw new RuntimeException("Cannnot set visible length for a word-wrapped Editable Label");
    } else {
      ((TextBox) this.changeText).setVisibleLength(length);
    }
  }

  /** Get the visible length of the editable area.
   * @return Visible length of editable area if not a word wrapped label.
   * @throws RuntimeExcpetion If editable label is word wrapped.
   */
  public final int getVisibleLength() {
    if (this.editableType != EditableType.TEXTBOX) { throw new RuntimeException("Cannnot get visible length for a word-wrapped Editable Label."); }
    if (this.text.getWordWrap()) {
      throw new RuntimeException("Cannnot get visible length for a word-wrapped Editable Label");
    } else {
      return ((TextBox) this.changeText).getVisibleLength();
    }
  }

  /** Constructor that changes default text for buttons and allows the setting of
   * the wordwrap property directly.
   * @param labelText The initial text of the label.
   * @param onUpdate Handler object for performing actions once label is updated.
   * @param okText Text for use in overiding the default OK button text.
   * @param cancelText Text for use in overiding the default CANCEL button text.
   * @param wordWrap Boolean representing if the label should be word wrapped or not
   */
  public EditableLabel(final String labelText, final String okText, final String cancelText,
      final boolean wordWrap) {
    createEditableLabel(labelText, okText, cancelText);
    this.text.setWordWrap(wordWrap);
  }

  /** Constructor that uses default text values for buttons and sets the word
   * wrap property.
   * @param labelText The initial text of the label.
   * @param onUpdate Handler object for performing actions once label is updated.
   * @param wordWrap Boolean representing if the label should be word wrapped or not
   */
  public EditableLabel(final String labelText, final boolean wordWrap) {
    createEditableLabel(labelText, this.defaultOkButtonText, this.defaultCancelButtonText);
    this.text.setWordWrap(wordWrap);
  }

  /** Constructor that changes default button text.
   * @param labelText The initial text of the label.
   * @param onUpdate Handler object for performing actions once label is updated.
   * @param okText Text for use in overiding the default OK button text.
   * @param cancelText Text for use in overiding the default CANCEL button text.
   */
  public EditableLabel(final String labelText, final String okText, final String cancelText) {
    createEditableLabel(labelText, okText, cancelText);
  }

  /** Constructor that uses default text values for buttons.
   * @param labelText The initial text of the label.
   * @param onUpdate Handler object for performing actions once label is updated.
   */
  public EditableLabel(final String labelText) {
    createEditableLabel(labelText, this.defaultOkButtonText, this.defaultCancelButtonText);
    setHasButtons(false);
  }

  /** Default constructor, it's set defaultOKButtonText and defaultCancelButtonText
   * and void text, and set hasButtons to false */
  public EditableLabel() {
    createEditableLabel("", this.defaultOkButtonText, this.defaultCancelButtonText);
    setHasButtons(false);
  }

  public void setHasButtons(final boolean hasButtons) {
    this.hasButtons = hasButtons;
    this.confirmChange.setVisible(hasButtons);
    this.cancelChange.setVisible(hasButtons);
  }
  public boolean isHasButtons() {
    return this.hasButtons;
  }

  @Override
  public HorizontalAlignmentConstant getHorizontalAlignment() {
    return this.text.getHorizontalAlignment();
  }

  @Override
  public void setHorizontalAlignment(final HorizontalAlignmentConstant align) {
    this.text.setHorizontalAlignment(align);
  }

  @Override
  public HandlerRegistration addValueChangeHandler(final ValueChangeHandler<E> handler) {
    return addHandler(handler, ValueChangeEvent.getType());
  }

  @Override
  public HandlerRegistration addClickHandler(final ClickHandler handler) {
    return addDomHandler(handler, ClickEvent.getType());
  }

  public final EditableType getEditableType() {
    if (this.editableType == null) {
      if (getWordWrap()) { this.editableType = EditableType.TEXTAREA; }
      else { this.editableType = EditableType.TEXTBOX; }
    }
    return this.editableType;
  }

  public final void setEditableType(final EditableType editableType) {
    this.editableType = editableType;
  }

  /** Type of editable widget */
  public enum EditableType {
    TEXTBOX, TEXTAREA, LISTBOX, ;
  }
}

